package asteroids.model;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.antlr.v4.runtime.RecognitionException;

import asteroids.CollisionListener;
import asteroids.IFacade;
import asteroids.ModelException;
import asteroids.model.programs.Program;
import asteroids.model.programs.ProgramFactoryImpl;
import asteroids.model.programs.expressions.Expression;
import asteroids.model.programs.parsing.ProgramParser;
import asteroids.model.programs.statements.Statement;
import asteroids.model.programs.types.Type;

public class Facade implements IFacade<World, Ship, Asteroid, Bullet, Program> {

	public Facade() {

	}

	@Override
	public World createWorld(double width, double height) {
		try {
			return new World(width, height);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public double getWorldWidth(World world) {
		return world.getDimension().getX();
	}

	@Override
	public double getWorldHeight(World world) {
		return world.getDimension().getY();
	}

	@Override
	public Set<Ship> getShips(World world) {
		return world.getAllShips();
	}

	@Override
	public Set<Asteroid> getAsteroids(World world) {
		return world.getAllAsteroids();
	}

	@Override
	public Set<Bullet> getBullets(World world) {
		return world.getAllBullets();
	}

	@Override
	public void addShip(World world, Ship ship) {
		try {
			world.add(ship);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public void addAsteroid(World world, Asteroid asteroid) {
		try {
			world.add(asteroid);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public void removeShip(World world, Ship ship) {
		try {
			world.remove(ship);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public void removeAsteroid(World world, Asteroid asteroid) {
		try {
			world.remove(asteroid);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public void evolve(World world, double dt,
			CollisionListener collisionListener) {
		try {
			world.evolve(dt, collisionListener);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public Ship createShip(double x, double y, double xVelocity,
			double yVelocity, double radius, double direction, double mass) {
		try {
			return new Ship(x, y, xVelocity, yVelocity, radius, mass, direction);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public boolean isShip(Object o) {
		return o instanceof Ship;
	}

	@Override
	public double getShipX(Ship ship) {
		return ship.getPosition().getX();
	}

	@Override
	public double getShipY(Ship ship) {
		return ship.getPosition().getY();
	}

	@Override
	public double getShipXVelocity(Ship ship) {
		return ship.getVelocity().getX();
	}

	@Override
	public double getShipYVelocity(Ship ship) {
		return ship.getVelocity().getY();
	}

	@Override
	public double getShipRadius(Ship ship) {
		return ship.getRadius();
	}

	@Override
	public double getShipDirection(Ship ship) {
		return ship.getAngle();
	}

	@Override
	public double getShipMass(Ship ship) {
		return ship.getMass();
	}

	@Override
	public World getShipWorld(Ship ship) {
		return ship.getWorld();
	}

	@Override
	public boolean isShipThrusterActive(Ship ship) {
		return ship.isThrusterActive();
	}

	@Override
	public void setThrusterActive(Ship ship, boolean active) {
		ship.setThruster(active);
	}

	@Override
	public void turn(Ship ship, double angle) {
		if(((Double)angle).isNaN() || ((Double)angle).isInfinite())
			throw new ModelException("Illegal value for angle!");
		ship.rotate(angle);
	}

	@Override
	public void fireBullet(Ship ship) {
		ship.fire();
	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius) {
		try {
			return new Asteroid(x, y, xVelocity, yVelocity, radius);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public Asteroid createAsteroid(double x, double y, double xVelocity,
			double yVelocity, double radius, Random random) {
		try {
			return new Asteroid(x, y, xVelocity, yVelocity, radius, random);
		} catch (IllegalArgumentException exc) {
			throw new ModelException(exc);
		}
	}

	@Override
	public boolean isAsteroid(Object o) {
		return o instanceof Asteroid;
	}

	@Override
	public double getAsteroidX(Asteroid asteroid) {
		return asteroid.getPosition().getX();
	}

	@Override
	public double getAsteroidY(Asteroid asteroid) {
		return asteroid.getPosition().getY();
	}

	@Override
	public double getAsteroidXVelocity(Asteroid asteroid) {
		return asteroid.getVelocity().getX();
	}

	@Override
	public double getAsteroidYVelocity(Asteroid asteroid) {
		return asteroid.getVelocity().getY();
	}

	@Override
	public double getAsteroidRadius(Asteroid asteroid) {
		return asteroid.getRadius();
	}

	@Override
	public double getAsteroidMass(Asteroid asteroid) {
		return asteroid.getMass();
	}

	@Override
	public World getAsteroidWorld(Asteroid asteroid) {
		return asteroid.getWorld();
	}

	@Override
	public boolean isBullets(Object o) {
		return o instanceof Bullet;
	}

	@Override
	public double getBulletX(Bullet bullet) {
		return bullet.getPosition().getX();
	}

	@Override
	public double getBulletY(Bullet bullet) {
		return bullet.getPosition().getY();
	}

	@Override
	public double getBulletXVelocity(Bullet bullet) {
		return bullet.getVelocity().getX();
	}

	@Override
	public double getBulletYVelocity(Bullet bullet) {
		return bullet.getVelocity().getY();
	}

	@Override
	public double getBulletRadius(Bullet bullet) {
		return bullet.getRadius();
	}

	@Override
	public double getBulletMass(Bullet bullet) {
		return bullet.getMass();
	}

	@Override
	public World getBulletWorld(Bullet bullet) {
		return bullet.getWorld();
	}

	@Override
	public Ship getBulletSource(Bullet bullet) {
		return bullet.getSource();
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> parseProgram(String text) {
		ProgramFactoryImpl factory = new ProgramFactoryImpl();
		ProgramParser<Expression, Statement, Type> parser = new ProgramParser<>(factory);
		try {
			parser.parse(text);
			List<String> errors = parser.getErrors();
			if(! errors.isEmpty()) {
				return ParseOutcome.failure(errors.get(0));
			} else {
				return ParseOutcome.success(new Program(parser.getGlobals(), parser.getStatement())); 
			}
		} catch(RecognitionException e) {
			return ParseOutcome.failure(e.getMessage());
		}
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromStream(InputStream stream)
			throws IOException {
	    java.util.Scanner scanner = new java.util.Scanner(stream);
		java.util.Scanner s = scanner.useDelimiter("\\A");
	    String text = s.hasNext() ? s.next() : "";
	    scanner.close();
	    return parseProgram(text);
	}

	@Override
	public asteroids.IFacade.ParseOutcome<Program> loadProgramFromUrl(URL url)
			throws IOException {
		try {
			InputStream is = url.openStream();
			return loadProgramFromStream(is);
		} catch (IOException exc) {
			return ParseOutcome.failure(exc.getMessage());
		}
	}

	@Override
	public boolean isTypeCheckingSupported() {
		return true;
	}

	@Override
	public asteroids.IFacade.TypeCheckOutcome typeCheckProgram(Program program) {
		//Not a complete typecheck, just a quick simulation
		Ship simulShip = new Ship(0, 0, 0, 0, 20, 1, 0);
		simulShip.setWorld(new World(1000, 1000));
		TypeCheckOutcome outcome = TypeCheckOutcome.success();
		try {
			simulShip.setProgram(program);
			// resume the program 100 times, rather arbitrarily
			for(int i=0; i< 100; i++) {
				program.resume();
			}
		}
		catch(Exception exc) {
			String message = exc.getMessage();
			if(exc.getCause() != null) {
				message += " caused by: "+exc.getCause().getMessage();
			}
			outcome = TypeCheckOutcome.failure(message);
		}
		return outcome;
	}

	@Override
	public void setShipProgram(Ship ship, Program program) {
		try {
			ship.setProgram(program);
		}
		catch(Exception exc) {
			throw new ModelException(exc);
		}
	}

}
